﻿using MongoDB.Bson;
using MongoDB.Driver;
using Sustainalytics.Entities;
using Sustainalytics.Insights.Service;
using Sustainalytics.Utils;
using Sustainalytics.Utils.Serilog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Sustainalytics.DataLayer
{
    public class InsightRepository : IInsightRepository, IHelperRepository
    {
        private readonly IMongoCollection<Insight> collection;
        private readonly IMongoDatabase database;
        private readonly string _databaseName;
        private readonly string className;

        public InsightRepository(string cnxStringMongo)
        {
            className = this.GetType().Name;

            if (string.IsNullOrEmpty(cnxStringMongo))
                throw new ArgumentException("cnxStringMongo");

            var url = new MongoUrl(cnxStringMongo);

            if (string.IsNullOrEmpty(url.DatabaseName))
                throw new ArgumentException("databaseName");

            var mongoClient = new MongoClient(cnxStringMongo);
            database = mongoClient.GetDatabase(url.DatabaseName);
            _databaseName = url.DatabaseName;
            collection = database.GetCollection<Insight>(typeof(Insight).Name);
            EnsureIndexOnTitleLowercase();
        }

        public virtual bool Create(Insight newInsight)
        {
            if (newInsight == null)
                throw new ArgumentNullException("newInsight");

            collection.InsertOne(newInsight);
            return true;
        }

        public virtual bool Update(Insight newInsight)
        {
            if (newInsight == null)
                throw new ArgumentNullException("newInsight");

            var q = Builders<Insight>.Filter.Eq(x => x.Id, newInsight.Id);
            var result = collection.FindOneAndReplace(q, newInsight);

            return result == newInsight;
        }

        public virtual bool Delete(Guid id)
        {
            throw new System.NotImplementedException();
        }

        public virtual bool Delete(List<Guid> ids)
        {
            if (ids == null)
                throw new ArgumentNullException("ids");

            collection.DeleteMany(Builders<Insight>.Filter.In(x => x.Id, ids));

            return true;
        }

        public virtual Insight Read(Guid id)
        {
            var res = collection.Find(x => x.Id == id).SingleOrDefault();
            return res;
        }

        private FilterDefinition<Insight> PrepareFilters(DateTime publishedOnDate, string titlePart, InsightState? state, Guid? insightTopicId, Guid? insightSectorId, DateTime noOlderThan)
        {
            var queryBuilder = new List<FilterDefinition<Insight>>();

            if (publishedOnDate != default(DateTime))
            {
                var one = Builders<Insight>.Filter.Eq(x => x.State, InsightState.Published);
                var two = Builders<Insight>.Filter.Lte(x => x.DatePublished, publishedOnDate.Date);
                var three = Builders<Insight>.Filter.Gte(x => x.DateUnPublished, publishedOnDate.Date);

                queryBuilder.Add(Builders<Insight>.Filter.And(one, two, three));
            }

            // all arguments are optional

            if (!string.IsNullOrEmpty(titlePart))
            {
                var regexFilter = new BsonRegularExpression(Regex.Escape(titlePart), "i");
                queryBuilder.Add(Builders<Insight>.Filter.Regex(x => x.Title, regexFilter));
            }

            if (state.HasValue)
            {
                queryBuilder.Add(Builders<Insight>.Filter.Eq(x => x.State, state.Value));
            }

            if (insightTopicId.HasValue)
            {
                queryBuilder.Add(Builders<Insight>.Filter.Eq(x => x.InsightTopicId, insightTopicId.Value));
            }

            if (insightSectorId.HasValue)
            {
                queryBuilder.Add(Builders<Insight>.Filter.Eq(x => x.InsightSectorId, insightSectorId.Value));
            }

            if (noOlderThan != default(DateTime))
            {
                queryBuilder.Add(Builders<Insight>.Filter.Gte(x => x.DatePublished, noOlderThan.Date));
            }

            if (!queryBuilder.Any())
                queryBuilder.Add(FilterDefinition<Insight>.Empty);

            return Builders<Insight>.Filter.And(queryBuilder);
        }

        //all params are filter and we apply AND for them
        //if a filter is not required place default value for it
        public virtual List<Insight> Read(DateTime publishedOnDate, string titlePart, InsightState? state, Guid? insightTopicId,
                                        Guid? insightSectorId, DateTime noOlderThan, int? skip, int? take, string orderby, bool orderAsc)
        {
            var filters = PrepareFilters(publishedOnDate, titlePart, state, insightTopicId, insightSectorId, noOlderThan);

            var cursor = collection.Find(filters);
            if (!string.IsNullOrEmpty(orderby))
                cursor = cursor.Sort((orderAsc) ? Builders<Insight>.Sort.Ascending(orderby) : Builders<Insight>.Sort.Descending(orderby));

            if (skip.HasValue && take.HasValue)
            {
                cursor.Skip(skip.Value);
                cursor.Limit(take.Value);
            }

            var result = cursor.ToList();
            return result;
        }

        public virtual long ReadCount(DateTime publishedOnDate, string titlePart, InsightState? state, Guid? insightTopicId, Guid? insightSectorId, DateTime noOlderThan)
        {
            // all arguments are optional
            var filters = PrepareFilters(publishedOnDate, titlePart, state, insightTopicId, insightSectorId, noOlderThan);
            var result = collection.Count(filters);

            return result;
        }

        public virtual bool Update(List<Tuple<Guid, InsightState>> list)
        {
            if (list == null)
                throw new ArgumentNullException("list");

            list.ForEach(x =>
               {
                   var update = Builders<Insight>.Update.Set(i => i.State, x.Item2);
                   var query = Builders<Insight>.Filter.Eq(i => i.Id, x.Item1);

                   collection.FindOneAndUpdate(query, update);
               }
            );
            return true;
        }

        public virtual List<InsightTopicName> GetTopicsName()
        {
            var coll = database.GetCollection<InsightTopicName>(typeof(InsightTopicName).Name);
            var res = coll.Find(_ => true).ToList();
            return res;
        }

        public virtual List<InsightSectorName> GetSectorsName()
        {
            var coll = database.GetCollection<InsightSectorName>(typeof(InsightSectorName).Name);
            var res = coll.Find(_ => true).ToList();
            return res;
        }

        public virtual List<Guid> ReadUnpublishedWithStatusActive(DateTime reference)
        {
            var q = Builders<Insight>.Filter.Where(x => x.DateUnPublished <= reference && x.State == InsightState.Published);
            var res = collection.Find(q).Project(x => x.Id).ToList();
            return res;
        }

        private void EnsureIndexOnTitleLowercase()
        {
            var idxname = "titlelowercaseidx";
            if (!UnitTestDetector.IsInUnitTest)
                collection.Indexes.CreateOne(Builders<Insight>.IndexKeys.Ascending(x => x.TitleLowercase), new CreateIndexOptions { Name = idxname });
        }
    }
}
